import json
import logging
from datetime import datetime
from typing import List, Optional

from sqlalchemy import delete, func, or_, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload

from app.enums.menu_type import MenuTypeEnum
from app.exceptions.base import AppException
from app.models.system import SysMenu, SysRole, SysRoleMenu
from app.schemas.menu import MenuForm, MenuVO, RouteMeta, RouteVO
from app.schemas.option import IntOption
from app.service.role_service import RoleService
from app.utils.str_utils import capitalize, to_camel_case

# 日志记录器
logger = logging.getLogger(__name__)


class MenuService:
    """菜单服务类：负责菜单数据的增删改查和树形结构构建"""

    def __init__(self, session: AsyncSession, role_service: RoleService):
        """初始化菜单服务

        Args:
            session: 数据库会话
        """
        self.session = session
        self.role_service = role_service

    async def get_menus(self, keyword: str = None) -> List[MenuVO]:
        """获取菜单

        Args:
            keyword: 关键字

        Returns:
            list[MenuVO]: 菜单列表
        """
        stmt = select(SysMenu)

        if keyword:
            stmt = stmt.where(SysMenu.name.ilike(func.concat("%", keyword, "%")))

        stmt = stmt.options(selectinload(SysMenu.children), selectinload(SysMenu.parent))

        stmt = stmt.order_by(SysMenu.sort)

        result = await self.session.execute(stmt)
        menus = result.scalars().all()

        if not menus:
            return []

        # 处理 parent_id 为 None 的情况
        for menu in menus:
            menu.parent_id = menu.parent_id if menu.parent_id is not None else 0

        menu_ids = [menu.id for menu in menus]
        menu_parent_ids = [menu.parent_id for menu in menus]
        root_ids = list(set([menu_parent_id for menu_parent_id in menu_parent_ids if menu_parent_id not in menu_ids]))

        # 获取根节点ID（递归的起点）
        # root_ids = list(set([menu.parent_id for menu in menus if menu.parent is None]))

        # 构建菜单树
        def build_tree(menus: List[SysMenu], parent_id: int) -> List[MenuVO]:
            """递归构建菜单树

            Args:
                menus: 所有菜单列表
                parent_id: 父级菜单ID

            Returns:
                List[MenuVO]: 构建好的菜单树
            """
            children = []
            for menu in menus:
                if menu.parent_id == parent_id:
                    # 手动构建 MenuVO 对象，避免直接验证 SQLAlchemy 模型
                    menu_vo = MenuVO(
                        id=menu.id,
                        name=menu.name,
                        parent_id=menu.parent_id,
                        type=menu.type,
                        route_name=menu.route_name,
                        route_path=menu.route_path,
                        component=menu.component,
                        sort=menu.sort,
                        visible=menu.visible,
                        icon=menu.icon,
                        redirect=menu.redirect,
                        perm=menu.perm,
                        children=None,  # 先设置为 None，递归构建子菜单
                    )
                    # 递归构建子菜单
                    _children = build_tree(menus, menu.id)
                    menu_vo.children = _children if _children else None
                    children.append(menu_vo)
            return children

        # 构建菜单树
        menu_tree = []
        for root_id in root_ids:
            menu_tree.extend(build_tree(menus, root_id))

        return menu_tree

    async def get_menu_options(self, only_parent: bool) -> List[IntOption]:
        """获取菜单选项
        Args:
            only_parent: 是否只获取父级菜单
        Returns:
            List[IntOption]: 菜单选项列表
        """
        stmt = select(SysMenu)
        if only_parent:
            stmt = stmt.where(SysMenu.type.in_([MenuTypeEnum.MENU.code, MenuTypeEnum.CATALOG.code]))
        stmt = stmt.options(selectinload(SysMenu.children), selectinload(SysMenu.parent))
        stmt = stmt.order_by(SysMenu.sort)
        result = await self.session.execute(stmt)
        menus = result.scalars().all()
        if not menus:
            return []
        # 处理 parent_id 为 None 的情况
        for menu in menus:
            menu.parent_id = menu.parent_id if menu.parent_id is not None else 0

        def build_options(menus: List[SysMenu], parent_id: int) -> List[IntOption]:
            """递归构建菜单选项
            Args:
                menus: 所有菜单列表
                parent_id: 父级菜单ID
            Returns:
                List[IntOption]: 构建好的菜单选项列表
            """
            options = []
            for menu in menus:
                if menu.parent_id == parent_id:
                    option = IntOption(value=menu.id, label=menu.name)
                    _children = build_options(menus, menu.id)
                    option.children = _children if _children else None
                    options.append(option)
            return options

        # 获取根节点ID（递归的起点）
        menu_ids = [menu.id for menu in menus]
        menu_parent_ids = [menu.parent_id for menu in menus]
        root_ids = list(set([menu_parent_id for menu_parent_id in menu_parent_ids if menu_parent_id not in menu_ids]))
        # 构建菜单选项树
        menu_options = []
        for root_id in root_ids:
            menu_options.extend(build_options(menus, root_id))
        return menu_options

    async def get_menu_by_id(self, menu_id: int) -> Optional[SysMenu]:
        """根据ID获取菜单详情

        Args:
            menu_id: 菜单ID

        Returns:
            Optional[SysMenu]: 菜单对象，如果不存在返回None
        """
        stmt = (
            select(SysMenu)
            .where(SysMenu.id == menu_id)
            .options(selectinload(SysMenu.children), selectinload(SysMenu.parent))
        )
        result = await self.session.execute(stmt)
        return result.scalar_one_or_none()

    async def get_menu_by_rolecode(self, role_codes: List[str], is_root: bool = False) -> Optional[List[SysMenu]]:
        """根据角色编码获取菜单详情
        Args:
            role_codes: 角色编码列表
            is_root: 是否为超级管理员
        Returns:
            Optional[List[SysMenu]]: 菜单对象，如果不存在返回None
        """
        if not is_root:
            stmt = (
                select(SysMenu)
                .join(SysRoleMenu, SysMenu.id == SysRoleMenu.menu_id)
                .join(SysRole, SysRoleMenu.role_id == SysRole.id)
            ).where(SysMenu.type != MenuTypeEnum.BUTTON.code, SysRole.status == 1, SysRole.is_deleted == 0)
            if role_codes:
                stmt = stmt.where(SysRole.code.in_(role_codes))
        else:
            stmt = (select(SysMenu)).where(SysMenu.type != MenuTypeEnum.BUTTON.code)
        stmt = stmt.options(selectinload(SysMenu.children), selectinload(SysMenu.parent))
        stmt = stmt.order_by(SysMenu.sort)
        result = await self.session.execute(stmt)
        return result.scalars().all()

    async def get_routes(self, role_codes: List[str], is_root: bool = False) -> List[RouteVO]:
        """根据角色编码获取菜单详情
        Args:
            role_codes: 角色编码列表
            is_root: 是否为超级管理员
        Returns:
            Optional[List[RouteVO]]: 菜单对象，如果不存在返回None
        """
        menus = await self.get_menu_by_rolecode(role_codes, is_root)
        if not menus:
            return []
        # 处理 parent_id 为 None 的情况
        for menu in menus:
            menu.parent_id = menu.parent_id if menu.parent_id is not None else 0

        def build_routes(menus: List[SysMenu], parent_id: int) -> List[RouteVO]:
            """递归构建菜单路由
            Args:
                menus: 所有菜单列表
                parent_id: 父级菜单ID
            Returns:
                List[RouteVO]: 构建好的菜单路由列表
            """
            routes = []
            for menu in menus:
                if menu.parent_id == parent_id:
                    route_name = menu.route_name if menu.route_name else to_camel_case(capitalize(menu.name))

                    route_vo = RouteVO(
                        path=menu.route_path,
                        name=route_name,
                        component=menu.component,
                        meta=RouteMeta(
                            title=menu.name,
                            icon=menu.icon,
                            hidden=(menu.visible == 0),
                            keepAlive=True if (menu.keep_alive == 1 and menu.type == MenuTypeEnum.MENU.code) else False,
                            alwaysShow=True if (menu.always_show == 1) else False,
                            params=json.loads(menu.params) if menu.params else None,
                        ),
                    )
                    _children = build_routes(menus, menu.id)
                    route_vo.children = _children if _children else None
                    routes.append(route_vo)
            return routes

        # 获取根节点ID（递归的起点）
        menu_ids = [menu.id for menu in menus]
        menu_parent_ids = [menu.parent_id for menu in menus]
        root_ids = list(set([menu_parent_id for menu_parent_id in menu_parent_ids if menu_parent_id not in menu_ids]))
        # 构建菜单路由
        menu_routes = []
        for root_id in root_ids:
            menu_routes.extend(build_routes(menus, root_id))
        return menu_routes

    async def save_menu(self, menu_form: MenuForm) -> SysMenu:
        """保存菜单信息

        Args:
            menu_form: 菜单表单数据，必须包含有效的菜单类型和父级ID

        Returns:
            SysMenu: 保存后的菜单对象

        Raises:
            AppException: 当输入验证失败或业务规则违反时抛出
            ValueError: 当参数类型或格式不正确时抛出
        """
        # 输入验证
        if not menu_form:
            raise ValueError("菜单表单数据不能为空")

        if not isinstance(menu_form, MenuForm):
            raise TypeError("menu_form 参数必须是 MenuForm 类型")

        menu_type = menu_form.type
        if menu_type not in [
            MenuTypeEnum.MENU.code,
            MenuTypeEnum.CATALOG.code,
            MenuTypeEnum.LINK.code,
            MenuTypeEnum.BUTTON.code,
        ]:
            raise AppException(message=f"无效的菜单类型: {menu_type}")

        # 菜单类型特定处理
        try:
            if menu_type == MenuTypeEnum.CATALOG.code:  # 目录类型
                if not menu_form.route_path:
                    raise AppException(message="目录类型必须指定路由路径")

                _path = menu_form.route_path
                if menu_form.parent_id == 0 and not _path.startswith("/"):
                    menu_form.route_path = f"/{_path}"
                menu_form.component = "Layout"

            elif menu_type == MenuTypeEnum.LINK.code:  # 外链类型
                menu_form.component = None

            elif menu_type == MenuTypeEnum.BUTTON.code:  # 按钮类型
                if not menu_form.perm:
                    raise AppException(message="按钮类型必须指定权限标识")
        except AttributeError as e:
            logger.error(f"菜单表单数据不完整: {e}")
            raise AppException(message="菜单表单数据不完整，请检查必填字段")

        # 防止循环引用
        if menu_form.id and menu_form.id == menu_form.parent_id:
            raise AppException(message="父级菜单不能为当前菜单")

        try:
            # 先生成菜单树路径
            tree_path = await self._generate_menu_tree_path(menu_form.parent_id)
            if tree_path is None and menu_form.parent_id != 0:
                raise AppException(message="指定的父级菜单不存在")

            # 处理路由参数
            params_str = None
            if menu_form.params:
                params = {}
                for item in menu_form.params:
                    if hasattr(item, "key") and hasattr(item, "value"):
                        params[item.key] = item.value
                    else:
                        logger.warning(f"忽略无效的路由参数项: {item}")
                params_str = json.dumps(params) if params else None

            # 准备验证数据，确保字段类型匹配
            validation_data = {"tree_path": tree_path, "params": params_str, "update_time": datetime.now()}

            # 分别处理新建和更新两种情况
            if menu_form.id is not None:
                # 更新菜单：查询现有记录并更新
                existing_menu = await self.get_menu_by_id(menu_form.id)
                if not existing_menu:
                    raise AppException(message=f"菜单不存在: ID={menu_form.id}")

                # 更新字段
                for field, value in validation_data.items():
                    if hasattr(existing_menu, field):
                        setattr(existing_menu, field, value)

                # 更新menu_form中的字段
                menu_form_data = menu_form.model_dump(exclude_unset=True)
                for field, value in menu_form_data.items():
                    if hasattr(existing_menu, field) and field != "id":
                        setattr(existing_menu, field, value)

                menu = existing_menu
            else:
                # 新建菜单：直接创建SysMenu实例
                menu_data = menu_form.model_dump(exclude_unset=True)
                menu_data.update(validation_data)
                # 移除id字段，让数据库自动生成
                menu_data.pop("id", None)
                menu = SysMenu(**menu_data)

            # 菜单类型为1时检查路由名称唯一性
            if menu_type == MenuTypeEnum.MENU.code:
                if not menu.route_name:
                    raise AppException(message="菜单类型必须指定路由名称")

                stmt = select(func.count()).where(SysMenu.route_name == menu.route_name)
                if menu_form.id:
                    stmt = stmt.where(SysMenu.id != menu_form.id)

                result = await self.session.execute(stmt)
                count = result.scalar_one()
                if count > 0:
                    raise AppException(message="路由名称已存在")
            else:
                menu.route_name = None

            # 保存到数据库
            self.session.add(menu)
            await self.session.commit()
            await self.session.refresh(menu)

            logger.info(f"菜单保存成功: ID={menu.id}, 名称={menu.name}, 类型={menu_type}")

            # 更新角色菜单关联缓存（如果是更新操作）
            if menu_form.id:
                try:
                    await self.role_service.refresh_role_perms_cache()
                    logger.debug("角色权限缓存已刷新")
                except Exception as cache_error:
                    logger.warning(f"刷新角色权限缓存失败: {cache_error}")

            # 更新子菜单的树路径
            try:
                await self._update_children_tree_path(menu.id, tree_path)
            except Exception as tree_error:
                logger.error(f"更新子菜单树路径失败: {tree_error}")
                # 不抛出异常，避免影响主流程

            return menu

        except AppException:
            # 业务异常直接抛出
            raise
        except Exception as e:
            # 数据库操作异常处理
            logger.error(f"保存菜单失败: {e}", exc_info=True)
            raise AppException(message="保存菜单失败，请稍后重试")

    async def _update_children_tree_path(self, menu_id: int, tree_path: str) -> None:
        """更新子菜单的 tree_path

        Args:
            menu_id: 父级菜单ID
            tree_path: 新的 tree_path

        Raises:
            Exception: 数据库操作失败时抛出
        """
        try:
            stmt = select(SysMenu).where(SysMenu.parent_id == menu_id)
            result = await self.session.execute(stmt)
            children = result.scalars().all()

            if children:
                child_tree_path = f"{tree_path},{menu_id}"
                update_stmt = update(SysMenu).where(SysMenu.parent_id == menu_id).values(tree_path=child_tree_path)
                await self.session.execute(update_stmt)

                # 递归更新子菜单的子菜单
                for child in children:
                    await self._update_children_tree_path(child.id, child_tree_path)

                await self.session.commit()
                logger.debug(f"已更新 {len(children)} 个子菜单的树路径")

        except Exception as e:
            logger.error(f"更新子菜单树路径失败 - 菜单ID: {menu_id}, 错误: {e}")
            raise

    async def _generate_menu_tree_path(self, parent_id: int) -> str:
        """生成菜单树路径

        Args:
            parent_id: 父级菜单ID

        Returns:
            str: 生成的树路径

        Raises:
            AppException: 当父级菜单不存在时抛出
        """
        if parent_id == 0:
            return f"{parent_id}"
        else:
            parent_menu = await self.get_menu_by_id(parent_id)
            if not parent_menu:
                raise AppException(message=f"指定的父级菜单不存在: {parent_id}")
            return f"{parent_menu.tree_path},{parent_menu.id}"

    async def delete_menu(self, menu_id: int) -> bool:
        """删除菜单（逻辑删除）

        Args:
            menu_id: 菜单ID

        Returns:
            bool: 删除是否成功
        """
        stmt = delete(SysMenu).where(
            or_(SysMenu.id == menu_id, func.concat(",", SysMenu.tree_path, ",").like(func.concat("%", menu_id, "%")))
        )
        result = await self.session.execute(stmt)
        await self.session.commit()
        if result.rowcount == 0:
            return False

        await self.role_service.refresh_role_perms_cache()

        return True

    # ------------------------------
    # 1. 构建 Option 树（下拉选项）
    # ------------------------------
    @classmethod
    def build_menu_options(cls, parent_id: Optional[int], menu_list: List[SysMenu]) -> List[IntOption]:
        """构建菜单选项树（用于下拉选择）

        Args:
            parent_id: 父级菜单ID
            menu_list: 菜单列表

        Returns:
            List[IntOption]: 选项树
        """
        options = []
        for menu in menu_list:
            if menu.parent_id == parent_id:
                option = IntOption(value=menu.id, label=menu.name)
                children = cls.build_menu_options(menu.id, menu_list)
                if children:
                    option.children = children
                options.append(option)
        return options

    # ------------------------------
    # 2. 构建 MenuVO 树（菜单展示）
    # ------------------------------
    @classmethod
    def build_menu_tree(cls, parent_id: Optional[int], menu_list: List[SysMenu]) -> List[MenuVO]:
        """构建菜单树形结构（用于前端展示）

        Args:
            parent_id: 父级菜单ID
            menu_list: 菜单列表

        Returns:
            List[MenuVO]: 菜单树
        """
        tree = []
        for menu in menu_list:
            if menu.parent_id == parent_id:
                menu_vo = MenuVO(
                    id=menu.id,
                    name=menu.name,
                    parent_id=menu.parent_id,
                    icon=menu.icon,
                    sort=menu.sort,
                    visible=menu.visible,
                    type=menu.type,
                )
                children = cls.build_menu_tree(menu.id, menu_list)
                if children:
                    menu_vo.children = children
                tree.append(menu_vo)
        return tree

    # ------------------------------
    # 3. 构建 RouteVO 树（前端路由）
    # ------------------------------
    @classmethod
    def _to_route_vo(cls, menu: SysMenu) -> RouteVO:
        """将菜单对象转换为路由VO对象

        Args:
            menu: 菜单对象

        Returns:
            RouteVO: 路由VO对象

        Raises:
            RuntimeError: 参数解析失败时抛出
        """
        # 生成 route name
        if menu.route_name:
            route_name = menu.route_name
        else:
            camel = to_camel_case(menu.route_path or "", "-")
            route_name = capitalize(camel) if camel else f"Menu{menu.id}"

        # 构建 meta
        meta = RouteMeta(
            title=menu.name,
            icon=menu.icon,
            hidden=(menu.visible == 0),
            keep_alive=(menu.type == 1 and menu.keep_alive == 1),
            always_show=(menu.always_show == 1),
            params={},
        )

        # 解析 params
        if menu.params:
            try:
                parsed = json.loads(menu.params)
                if isinstance(parsed, dict):
                    meta.params = {k: str(v) for k, v in parsed.items()}
                else:
                    raise ValueError("params must be a JSON object")
            except (json.JSONDecodeError, ValueError, TypeError) as e:
                logger.error(f"解析菜单参数失败: {menu.params}, 错误: {e}")
                raise RuntimeError(f"解析菜单参数失败: {menu.params}") from e

        return RouteVO(
            name=route_name,
            path=menu.route_path or "",
            redirect=menu.redirect,
            component=menu.component,
            meta=meta,
            children=[],
        )

    @classmethod
    def build_routes(cls, parent_id: Optional[int], menu_list: List[SysMenu]) -> List[RouteVO]:
        """构建前端路由树

        Args:
            parent_id: 父级菜单ID
            menu_list: 菜单列表

        Returns:
            List[RouteVO]: 路由树
        """
        routes = []
        for menu in menu_list:
            if menu.parent_id == parent_id:
                route_vo = cls._to_route_vo(menu)
                children = cls.build_routes(menu.id, menu_list)
                if children:
                    route_vo.children = children
                routes.append(route_vo)
        return routes

    async def get_menu_form(self, menu_id: int) -> Optional[MenuForm]:
        """获取菜单表单数据
        Args:
            menu_id: 菜单ID
        Returns:
            Optional[MenuForm]: 菜单表单数据
        """
        menu = await self.get_menu_by_id(menu_id)
        if not menu:
            return None
        menu_vo = MenuForm.model_validate(menu)
        return menu_vo

    async def update_menu_visible(self, menu_id: int, visible: int) -> bool:
        """更新菜单的可见性
        Args:
            menu_id: 菜单ID
            visible: 可见性（1: 可见，0: 隐藏）
        Returns:
            bool: 更新是否成功
        """
        stmt = update(SysMenu).where(SysMenu.id == menu_id).values(visible=visible)
        await self.session.execute(stmt)
        await self.session.commit()
        return True
